個人的にUnity C#に積みたいもの
概要
超絶端的にいうと、C#のasync/awaitはUnityやゲームクライアントで使うにはコード量が必要すぎるのと汚染が激しすぎるのと呼ばれる側のコード量がわんさかしてて、
総合するとUnityで使うには価値が低すぎるので、
(中略)
メインスレッドをブロックしてもペナルティがない機能が欲しい。
思考実験
適当な思考実験、ブロック可能性みたいなのをUnity C#でUnityが提供する関数での始動で実現できればそれでいいのでは、、? というのがある。
これは果たせると嬉しいポイントとして、「go言語のいいところをパクりたい」というのがある。
StartCoroutine(IEnumerator i)関数はC#には存在しないがUnity C#には存在する関数で、これは「Unityのメインスレッドのどこかでiを実行する」というもの。で、当然ブロックしない。
で、「これに近い手段」で、「メインスレッドでできる機能特性を保ったまま、workerスレッドで」、「メインスレッドをブロックする」、というのが実現できると、
go言語の go 関数に近いことができて楽しいのでは?っていう。
こういう仮想コードを考えてる。
// 呼ばれる側
[Blockable]// この関数でblockしても困んない用のUnity Attribute
int Something ()
{
var cor = IEnumerator a ()
{
yield return null;
}
// これが、まあ、ほしいやつ。渡した処理が完了するまでブロックする。実行コンテキストはworker threadとかそういうやつ。
StartBlocking(cor());
// 上記のStartBlockingメソッドの中身はなんかこういうやつ
// var c = cor();
// while (c.MoveNext()){ }// この行でブロックするが、ブロックの待ちとか実行スレッドをUnityがマネージしてくれる。
// 上の行が終わったらここにくる
return 0;
}
// 呼ぶ側
var result = Something();// この関数にBlockableが付いてるのでブロックする。
みたいな書き方ができて、なおかつBlockableアトリビュートがついてる関数がブロックしても問題ない感じにUnityがコンパイルしてくれる、とかがあると、成立する気がする。
StartBlocking関数の中身の実行自体はUnityがworker threadに対して振ってくれるとうれしい。そしたらUnity APIを呼ぶのもかなり自由度が稼げる。
あとはアレなー、軽量スレッドの仕組みをパクって欲しいんだよな。worker threadをそのままStartBlockingコールと1:1で使うんではなく、ある程度の抽象性みたいなやつを組ませたい。
これは余談だが、別にUnityが提供している今のままのPlayerLoopとかに対してどんだけasyncしようが、特に性能的な価値はない、というのがある。
事実そういう書き方ができるライブラリは存在してるが、計算力的なアドバンテージは皆無で、
「CPU的な負荷が一切得しない状態でコード記述量を増やして見かけの非同期性を増しているが、実行コンテキストが一個なので別に速くならんし、本当に無駄な努力お疲れ様です」
となってしまっている。async/awaitを書くことそのものに価値を感じるのはさらに輪をかけて相当にダメだが。。。
現在の仮想コードのやつは、記法としてはSwiftのasync/awaitのproposalにちょっとだけ近い。
もっとGoな手法に寄せるとすごく、「StartBlocking関数を呼ぶ側にコントロール権がガンガン残せる」のでいいな~というのがある。例えばタイムアウトとかね。
StartBlocking関数は名前が適当だが、IEnumerator a をUnity ManagedなNativeスレッドで実行してくれる関数なので、
単純にBlockableがついてる関数を特別扱いしてくれれば(while条件待ちをしてもブロックしないで最終的にメインスレッド収束をやってくれれば)それで済むのでは、という気はしている。
これだと、Taskとかのwrap型の中に成功失敗をパラメータとして封入せずに済む、とか、コード読まなくても済むとかがある。あとキャンセルとかエラーな。
あとは関数の切れ目、どこを関数として切り出すか、とかは、まだまだデザイン的な考慮の猶予がある。
根本的に現在の仮想コードは、まだまだC#に近くて、ノイズが多い。もっと簡単に書けるようにできるはずだ。
AsyncOperationがかなりまあそういう系統の理想系なんだよな~~あれを満たすものを自作しないで済むようにできれば。
async/awaitをUnity mainthreadと没交渉なC#の言語機能だとするなら、今考えてるやつはUnityにしか関係ない言語機能であって欲しい。
現時点での仮想コードの根本的なアイデア
Unity C#は通常のC#とは違い、Main関数起点ではなくMonoBehaviourの各種メソッドを起点として実行されている。
これは裏を返せば、全ての関数の起点はUnityのMonoBehaviourであって例外はないので、普通に呼び出しても特殊なAttrで特殊な挙動をする関数、とかも結局果たせるんじゃねえの?って思っている。
まあリフレクションとかも結局実行コンテキストはあるんでアレだが、そもそもIL2CPPがある時点でEmitとかすら効かないので、C#Runtimeとしての厳密性みたいなのはわりかしどうでもいい感じ。
結局やりたいことは非同期による分散処理や計算資源を最大限活用することなので、記法として直列待ちかどうかとかはどうでもいいんだよな、、
メインスレッドをブロックしても固まらなくしたい、っていうのがまあ上記のような形である。
これができると、非同期を待つ側を、そこに全てを記述した上で、ガチで同期なスタイルで記述できる。
だいたいの言語の発想は逆で、非同期を待つ側が非同期になり、非同期の先が同期になる。
この発想なので、非同期を待つ側にcallbackやawaitが生える。
つまり、待つ側は非同期に待てるようなスタイル、デザインを要求される。
が、Goはとてもいい感じで、非同期を待つ側がガチでブロックしても問題がないデザインになっている。
for無限ループやselectでコンテキスト自体の進行をブロックすることに一切の躊躇がない。
そんで非同期処理自体はgo 関数 という形で記述できて、向こう側の書き方も同期スタイル、、なので、待つ側も動く側も全員同期スタイルで書ける、っていうのを果たしている。
あれは軽量スレッドだからできてるワッザという認識なんだけど、まあつまり、workerに対して実行する対象を抽象化~とかすると、だいたいフィーリングで=になるんじゃねえかなーって思っている。
もともとどこでだって同期的な書き方で待てるんだから、それでいいんじゃねえ?っていう。
これができると、まあぶっちゃけてasync汚染とか、非同期に特別なwrap型が必要なのとか、それらを扱うためのinterface実装が必要な前提をガンガン破壊できる。
無駄な努力を生まれる前に消せる。
goから学べるデザインにはさらに利点があり、「限界ギリギリまで非同期にするかどうかを悩める」というのがあって、あのへん参考にならんかなーというのも面白そう。
Unityの場合はmain threadの価値がまだまだ高いので、その利点を得たまんま、さらにgoのような特性を得ることができないかなーというのがある。
例えば、goは呼ぶ側にコントロール権がガンガン残せる、という利点があって、これはなんかマジでC#に欠けてる視点なので、
FPSがあり、ネイティブプラットフォームがあり、描画すべき画面があるゲームという土壌でなんかいい感じにできるのでは~という感じがしている。
色々見てるところ
特別なエントリーポイントと特性自体は必要なんだけど、こう、まあ、そういうかんじ。
実際色んな言語をつらつら見てウンウンいってる段階。
Unityの場合はC#という土壌がmanagedレイヤなのと、JobSystemとかでnativeレイヤに処理を渡すことで最大限のプラットフォーム火力を出していて、こういうのがあると楽だなー感がある。
どうせC# async/awaitはその価値がUnity上でnative並みになることは未来永劫無限に無い、という読みがあるんで、こういうのに投資するのはアリかもなー。単純に難しい。
(XNAを思い出す。C#でのマルチスレッドがnativeと同等になるのは、結局ローレイヤーがC#で書かれてないから無理だし無駄、っていう話。)
こういう話するのは面白いかもなー。